我使用gemini提取出了老师视频中展示的文档,非常好。因为老师确实没有给文档,blog里面也搜不到,幸好有了AI,帮我很大的忙。
Install NodeJS and Database 前提是要安装nodejs和某种数据库,因为prisma通常是为关系型数据库构建的,所以NoSQL这些大部分都不能支持,只支持MongoDB。我按照老师的要求,安装了postgresql。
npm init -y ,初始化项目。
npm i --save-dev prisma typescript ts-node @types/node nodemon , Install dependencies
Create tsconfig.json
xxxxxxxxxx91{2 "compilerOptions": {3 "sourceMap": true,4 "outDir": "dist",5 "strict": true,6 "lib": ["esnext"],7 "esModuleInterop": true8 }9}这个是配置typescript相关的。
npx prisma init --datasource-provider postgresql , Initial Prisma project
Connect Database
a. Install VSCode extension for Prisma
b. Also talk about auto formatting and npx prisma format
xxxxxxxxxx131// schema.prisma2datasource db {3 provider = "postgresql"4 url = env("DATABASE_URL")5}67generator client {8 provider = "prisma-client-js"9 output = "../generated/prisma"10}1112// .env13DATABASE_URL="postgresql://postgres:password@localhost:5433/test"这里需要重点讲解一下,datasource db指的是Prisma orm该怎么样连接到数据库管理系统,provider指的是数据库管理系统的类型,可以是mysql、sqlite等等,url指的是连接的数据库地址。
在步骤4执行之后,会生成schema.prisma文件和.env文件,在.env文件里面,有一个DATABASE_URL的变量,这里存放的是数据库的地址。这个地址可以是本地数据库的地址,也可以是云服务器中数据库的地址。

generator的作用是定义代码生成规则,provider指定用哪一个生成器,output自定义生成文件的输出目录。因为prisma的核心是“基于schema自动生成代码”,而generator就是“告诉prisma”要生成什么,怎么生成,生成到哪里“的关键配置。
只有这样配置了,在代码中才能使用PrismaClient,来方便的操作数据库。
url的格式是:postgresql://用户名:密码@主机:端口/数据库名,我的本地用户名默认是postgres,密码是123456,主机是localhost,端口是5432,数据库需要新建,打开powershell,输入psql -h localhost -p 5432 -U postgres,输入密码123456,然后输入create database prisma,这样就生成了一个名称为prisma的数据库。
那么这里的url就写成这样DATABASE_URL="postgresql://postgres:123456@localhost:5432/prisma?schema=public",这里的?schema=public,它的意思是告诉 Prisma(或其他工具):默认使用 public 这个 schema 来查找表和执行查询。这涉及到postgresql的表结构,和MySQL是不一样的,先不用管。


Create User Schema with name
xxxxxxxxxx41model User {2 id Int @id @default(autoincrement())3 name String4}npx prisma migrate dev --name init , Create/Run migration。这一步是为了将定义的model迁移到数据库中去,dev表示只在开发环境做这件事,--name init表示为这次操作设置名称,方便以后查看。
这一步执行之后,会在prisma文件夹里面生成一个migrations文件夹,里面就是迁移文件,这些文件将与postgresql交互并进行用户指定的修改。
与此同时,也对Prisma client进行了更新,这样使用prisma client的时候,就能够用到最新的数据库。

npm install @prisma/client - Install Client (Talk about how this also generates the client as well for us and how this generation happens every time you migrate as well and how you can create this generation your self with npx prisma generate)
创建一个script.ts文件,就可以写代码了。
xxxxxxxxxx151import { PrismaClient } from '@prisma/client'23const prisma = new PrismaClient({ log: ["query"] })45async function main() {6 // ... you will write your Prisma Client queries here7}89main()10 .catch((e) => {11 console.error(e.message)12 })13 .finally(async () => {14 await prisma.$disconnect()15 })await prisma.user.create({ data: { name: "Sally" } }) - Create new user
await prisma.user.findMany() - Get all users
Must have at least one generator but can have more than one
The provider represents what generator to use
a. This can be any NPM library such as a GraphQL generator
你想要的 PostgreSQL 类型 Prisma 写法(强烈推荐) 说明(精度 / 时区) 推荐指数 timestamp with time zone(最推荐) DateTime @db.Timestamptz 保存 UTC 时间,带时区无关,精度 6 位(微秒) 5 stars timestamp without time zone DateTime @db.Timestamp 保存本地时间,不带时区,容易踩坑 2 stars timestamptz(简写,同第 1 种) DateTime(不写任何 @db 也行) Prisma 默认就是 timestamptz! 5 stars date(只存日期,不带时间) DateTime @db.Date 相当于 2025-12-05,不带时间和时区 4 stars time(只存时间,不带日期) DateTime @db.Time 13:14:15,精度到微秒 3 stars timetz(带时区的时间) DateTime @db.TimeTz 很少用 1 star xxxxxxxxxx121model User {2id String @id @default(cuid())34createdAt DateTime @default(now()) // 自动就是 timestamptz5updatedAt DateTime @updatedAt // 自动就是 timestamptz67birthday DateTime @db.Date // 只存年月日,比如 1998-08-088lastLogin DateTime? // 可空,带时区的时间戳910// 如果你真的想明确写出来(可读性更好)11loginAt DateTime @db.Timestamptz12}
Post)基本语法:
xxxxxxxxxx81@relation(2name?: String,3fields?: String[],4references?: String[],5onDelete?: ReferentialAction,6onUpdate?: ReferentialAction,7map?: String8)最重要的三个参数:
参数 类型 必填? 放在哪一边? 说明与取值 name String(字符串字面量) 选填(但强烈建议写上) 两边都要写(且相同) 关系名字,用于消除歧义;迁移时保持关系稳定;精准include/select。 fields String[](字段名数组) 必须(有外键的一方) 外键所在的一方(多的一方) 指明本模型里哪个字段是外键 references String[](字段名数组) 必须(有外键的一方) 外键所在的一方 指向对端模型的哪个字段(通常是 @id 或 @@id) 这里的name可以用于精准include/select,这种需求在项目中是很常见的:
在多对多关系中,指定name参数,prisma会用这个name为我们创建一个中间表。
多对多关系的
@relation指令不需要显式地定义fields和references参数。xxxxxxxxxx111model Student {2id Int @id @default(autoincrement())3name String4courses Course[] @relation("Enrollment")5}67model Course {8id Int @id @default(autoincrement())9title String10students Student[] @relation("Enrollment")11}解释:
@relation("Enrollment")中的name是 "Enrollment"。这意味着 Prisma 会为Student和Course之间的多对多关系创建一个中间表,名称会是Enrollment。- 通过使用
name,我们可以明确中间表的名称,而不依赖 Prisma 默认的命名规则。查询:
xxxxxxxxxx51const students = await prisma.student.findMany({2include: {3courses: true, // 通过 include 加载关联的课程4},5})xxxxxxxxxx251model User {2id String @id @default(cuid())34// 我写的文章(一对多)5authoredPosts Post[] @relation("AuthoredPosts")67// 我点赞的文章(隐式多对多)← 两边 name 完全一样8likedPosts Post[] @relation("UserLikedPosts")910createdAt DateTime @default(now())11}1213model Post {14id String @id @default(cuid())15title String1617// 作者(一对多)18authorId String19author User @relation("AuthoredPosts", fields: [authorId], references: [id])2021// 被谁点赞(隐式多对多)← 两边 name 完全一样,不能写 fields/references22likedBy User[] @relation("UserLikedPosts")2324@@index([authorId])25}xxxxxxxxxx181const user = await prisma.user.findUnique({2where: { id: "clxxxxxx" }, // 必须加 where!3include: {4authoredPosts: true, // 我写的5likedPosts: true, // 我点的赞6},7})89// 或者分开查10const written = await prisma.user.findUnique({11where: { id },12select: { authoredPosts: { take: 10 } }13})1415const liked = await prisma.user.findUnique({16where: { id },17select: { likedPosts: { take: 20 } }18})如果两个关系都没写 name,include 里只能写 posts,要么全拿,要么全不拿,完全无法分开。
一、什么时候可以不写 @relation?(最常见的情况)
xxxxxxxxxx111model User {2id String @id @default(cuid())3posts Post[] // 这一边啥也不写4}56model Post {7id String @id @default(cuid())89authorId String10author User @relation(fields: [authorId], references: [id])11}结论:只要关系是「有外键的一方」写了 fields 和 references,另一方(User)可以完全不写 @relation,Prisma 自动推断,100% 正常工作。 这是目前 90% 项目都在用的最简写法。
二、什么时候必须写 @relation?(4 种硬性场景)
场景 必须写 @relation 的原因 正确写法示例 1. 自关联(同一个表里关联自己) 不写会报错 “Ambiguous relation” 见例子1 2. 一对多双向,但两边外键字段名不一致 Prisma 无法自动匹配 见例子2 3. 一个模型被多个关系引用(多对一多条) Prisma 不知道 Post[] 对应哪条关系 见例子3 4. 多对多显式关系(中间表) 必须写 name 来配对 三、四大场景详细写法
场景1:自关联(粉丝、关注、上下级、树形评论)
xxxxxxxxxx131model User {2id String @id @default(cuid()34// 我关注的(我 → 对方)5following User[] @relation("FollowRelation", fields: [followingId], references: [id])6followingId String?78// 关注我的(对方 → 我)9followers User[] @relation("FollowRelation")1011// 推荐:加索引12@@index([followingId])13}关键:两边都用同一个名字 "FollowRelation",不然会报错。
场景2:外键字段名不是默认的 authorId
xxxxxxxxxx111model Post {2id String @id @default(cuid())3title String4creator User @relation(fields: [createdById], references: [id])5createdById String // ← 不是 authorId!6}78model User {9id String @id @default(cuid())10posts Post[] @relation("CreatedPosts") // 必须写 name,否则找不到11}场景3:一个模型被多个关系引用(最容易踩坑!)
xxxxxxxxxx151model User {2id String @id @default(cuid())34writtenPosts Post[] @relation("Author") // 我写的文章5likedPosts Post[] @relation("Likes") // 我点赞的文章6}78model Post {9id String @id @default(cuid())1011author User @relation("Author", fields: [authorId], references: [id])12authorId String1314likedBy User[] @relation("Likes")15}不写 name 的话 Prisma 会报:Error: Ambiguous relation on model User.posts
场景4:显式多对多(中间表自己控制)
xxxxxxxxxx171model User {2id String @id @default(cuid())3followedBy Follow[] // 我被谁关注4following Follow[] // 我关注了谁5}67model Follow {8followerId String9followingId String1011follower User @relation("Following", fields: [followerId], references: [id])12following User @relation("FollowedBy", fields: [followingId], references: [id])1314followedAt DateTime @default(now())1516@@id([followerId, followingId]) // 复合主键17}
先不用管那么多,先把一对多的关系用起来,只在多的那一方写@relation即可。
xxxxxxxxxx231model Album {2 id Int @id @default(autoincrement())3 title String4 createdAt DateTime @default(now()) @map("created_at")56 photos Photo[]78 @@map("albums")9}1011model Photo {12 id Int @id @default(autoincrement())13 title String14 url String?15 thumbnailUrl String? @map("thumbnail_url")1617 albumId Int @map("album_id")18 album Album @relation(fields: [albumId], references: [id])1920 createdAt DateTime @default(now()) @map("created_at")2122 @@map("photos")23}在多的这一方:
xxxxxxxxxx21 albumId Int @map("album_id")2 album Album @relation(fields: [albumId], references: [id])Photo这张表里面,只会存albumId,不会存album这个值。这其实与数据库操作语言来定义是一致的,只不过 多的一方 加了album Album @relation(fields: [albumId], references: [id]),一的一方 加了photos Photo[],这其实是为了prisma能够更好操作,这里的album和photos在prisma中都属于字段名(虽然没有真正存在数据库表中去),在include操作的时候,可以当作key来传进去的。后面你就知道了。
那么在postgresql原生语言里面,定义一对多是什么样的呢?
xxxxxxxxxx201-- 1. 创建“一”的一方:用户表2CREATE TABLE users (3 id SERIAL PRIMARY KEY, -- 自增主键4 username VARCHAR(50) NOT NULL,5 email VARCHAR(100) UNIQUE NOT NULL6);78-- 2. 创建“多”的一方:帖子表9CREATE TABLE posts (10 id SERIAL PRIMARY KEY,11 title VARCHAR(255) NOT NULL,12 content TEXT,13 user_id INTEGER NOT NULL, -- 存储关联用户 ID 的列1415 -- 定义外键约束16 CONSTRAINT fk_user 17 FOREIGN KEY(user_id) 18 REFERENCES users(id) 19 ON DELETE CASCADE -- 如果用户被删除,其所有帖子也自动删除20);可以看的,在原生语言里面,只需要定义一个外键、定义外键约束即可。
album这个值和Album里面的photos Photo[],只是指定了关系。搞清楚这一点之后,其实各种表的关系也就很容易理解了。
Prisma 的关系字段分成两种:
| 类型 | 写法示例 | 存在于 Prisma Client 中 | 存在于数据库中 | 作用 |
|---|---|---|---|---|
| Relation scalar field | album Album @relation(...) | 是(可以 .album 访问) | 否 | 描述“这个 Photo 属于哪个 Album” |
| Relation list field | photos Photo[] | 是(可以 .photos 查询) | 否 | 描述“这个 Album 拥有哪些 Photos” |
| 实际存储字段 | albumId Int | 是 | 是 | 真正保存关系的字段(外键) |
符号就下面两个,另外还有两个方法。
[]
a. Turns the column into a list. When used on a reference type it creates all needed foreign keys and such
b. Can only be used on scalar types if the DB supports it
?
a. Makes a field optional
b. Cannot be used with []
| 修饰符 | 写法位置 | 含义 | 数据库表现(PostgreSQL) | 常用场景 |
|---|---|---|---|---|
| ? | 放在类型后面 | 可选字段(可为 null) | nullable = true | 头像、昵称、手机号、描述等非必填字段 |
| [] | 放在类型后面 | 数组(List) | text[]、int4[]、timestamptz[] 等 | 标签、角色列表、图片数组、爱好等 |
| @default() | 字段属性 | 默认值 | DEFAULT ... | 状态、积分、排序、是否启用等 |
| @updatedAt | 字段属性 | 自动更新为当前时间 | ON UPDATE NOW() | 任何需要记录最后修改时间的字段 |
Attributes have two levels
a. Field attributes apply to one field @
b. Block attributes apply to the entire block @@
字段属性就使用一个@符号,model属性就使用两个@@符号。
@id
a. All tables must have an id field or have a unique field
b. Can be combined with @default
i. autoincrement()-这个在postgresql里面就是serial类型了。
ii. uuid()
iii. cuid()-prisma推荐使用的id,就用这个
@@id
a. Used to create composite ids @@id([FirstName, LastName]) 这个符号用来创建组合id
@default
a. Can be passed a static value or a function such as now()
@unique - Makes a field unique
@@unique - Makes a combination of fields unique @@unique(authorId, postId) 创建组合唯一字段
@@index - Create index on one or more fields @@index(authorId, postId) 创建多个索引
@updatedAt - Automatically sets a field to the current time when you update the row
@relation
a. The fields with relation types are not actually in the db
b. One To Many
xxxxxxxxxx111model User {2 id Int @id @default(autoincrement())3 posts Post[]4}56model Post {7 id Int @id @default(autoincrement())8 9 author User @relation(fields: [authorId], references: [id])10 authorId Int11}c. One To One
xxxxxxxxxx111model User {2 id Int @id @default(autoincrement())3 profile Profile?4}56model Profile {7 id Int @id @default(autoincrement())8 9 userId Int @unique10 user User @relation(fields: [userId], references: [id])11}d. Many To Many
xxxxxxxxxx91model Post {2 id Int @id @default(autoincrement())3 categories Category[]4}56model Category {7 id Int @id @default(autoincrement())8 posts Post[]9}e. The @relation field must take a field and references value.
i. The field points to the field on the current model
ii. The references points to the field on the other model the field value references
f. If you have multiple relations on the same model you need to pass a name to the references attribute to disambiguate them
xxxxxxxxxx151model User {2 id Int @id @default(autoincrement())3 writtenPosts Post[] @relation("WrittenPosts")4 favoritePosts Post[] @relation("FavoritePosts")5}67model Post {8 id Int @id @default(autoincrement())9 10 authorId Int11 author User @relation("WrittenPosts", fields: [authorId], references: [id])12 13 favoritedById Int?14 favoritedBy User? @relation("FavoritePosts", fields: [favoritedById], references: [id])15}这个多对多的关系,看上去很复杂,但是用起来非常方便,就看速度怎么样了。Post的CUD操作没有什么影响,把authorId和favoritedById传递过来就行了。但是查询的时候,如果想查询具体的author和favoritedBy,那么就需要使用
include。xxxxxxxxxx71const postWithDetails = await prisma.post.findUnique({2where: { id: 101 },3include: {4author: true, // 获取作者详情5favoritedBy: true // 获取收藏者详情6}7});
就是User查询的时候,是怎么获取到writtenPosts和favoritePosts的?我感觉有点难,因为prisma在这里做了抽象了。
查询语句很简单:
xxxxxxxxxx71const userWithPosts = await prisma.user.findUnique({2where: { id: 1 },3include: {4writtenPosts: true, // 获取发布的文章5favoritePosts: true // 获取收藏的文章6},7});甚至可以在include里面写查询条件:
xxxxxxxxxx141const userWithFilteredPosts = await prisma.user.findUnique({2where: { id: 1 },3include: {4writtenPosts: {5take: 5,6orderBy: { id: 'desc' }7},8favoritePosts: {9where: {10title: { contains: '量化' }11}12}13},14});很简单吧。
那么原生语言里面,怎么查询呢?好像我没有查询过多对多关系。其实很简单,使用left join即可:
xxxxxxxxxx31SELECT u.id as user_id, p.* FROM users u2LEFT JOIN posts p ON u.id = p.author_id3WHERE u.id = 1;PostgreSQL 强大的 JSON 处理能力可以让你在一个查询中直接返回结构化的对象,这和 Prisma 的
include效果几乎一模一样。因为MySQL里面不支持JSON数据类型,所以我之前没有看到过这种写法。131SELECT2u.id,3-- 将作品聚合成 JSON 数组4(SELECT json_agg(p.*)5FROM posts p6WHERE p.author_id = u.id) AS written_posts,78-- 将收藏聚合成 JSON 数组9(SELECT json_agg(p.*)10FROM posts p11WHERE p.favorited_by_id = u.id) AS favorite_posts12FROM users u13WHERE u.id = 1;
非常重要:在
include语法中,我一直都没有搞清楚key应该是什么?这里说清楚,就是定义model时,表示关系的字段(不是xxxId这个字段,而是它链接的字段),这是prisma为了方便查询给我们的语法。
我一直以为key是
@relation("yyyyy", ...)这里面的第一个参数值,其实不是。要区分 “关系名(Relation Name)” 和 “字段名(Field Name)”。在 Prisma 的
include语法中,你需要填写的是当前模型(Post)中定义的成员属性名称,而不是关系背后的标签名。你在代码中看到的
@relation("WrittenPosts", ...)只是一个“链接标签”,不要在include里面使用它。它的唯一作用是告诉 Prisma:“Post 模型里的 author 字段,对应的正是 User 模型里的 writtenPosts 字段,它们是一根线上的两端。仅仅是给 Prisma 编译器看的,用来防止它搞混多条指向同一个模型的路径。”视角决定名称
在你的 Schema 中,两张表就像硬币的两面,视角不同,看到的字段名就不同:
从
User的视角看(你想获取用户关联的内容):你要
include的是User模型里的字段:
writtenPostsfavoritePosts从
Post的视角看(你想获取文章关联的内容):你要
include的是Post模型里的字段:
authorfavoritedBy这些都是prisma为我们做好的查询语法,用起来非常方便。
完全列表(官方所有字段属性,一共就这些)
| 分类 | 属性名称 | 示例 |
|---|---|---|
| 主键 | @id, @@id([]) | |
| 唯一约束 | @unique, @@unique([]) | |
| 默认值 | @default(), @updatedAt | |
| 数据库映射 | @map(), @@map() | |
| 数据库类型 | @db.Xxx(所有 PostgreSQL 类型都支持) | @db.Timestamptz, @db.Uuid, @db.VarChar(100) |
| 索引 | @@index([]), @@index([], type: Xxx) | GIN / Hash / Brin / Gist |
| 全文搜索 | @@fulltext([]) | |
| 关系 | @relation() | |
| 忽略字段 | @ignore | |
| 原生默认 | @default(dbgenerated("...")) |
举例说明:
| 频率 | 属性写法 | 作用 | 典型值 / 示例 | 必背程度 |
|---|---|---|---|---|
| 1 | @id | 声明主键(单字段) | @id | 5 stars |
| 2 | @@id([field1, field2]) | 复合主键 | @@id([tenantId, id]) | 5 stars |
| 3 | @unique | 单字段唯一约束 | email String @unique | 5 stars |
| 4 | @@unique([field1, field2]) | 复合唯一约束 | @@unique([tenantId, email]) | 5 stars |
| 5 | @default(value) | 默认值 | @default(cuid())、@default(now())、@default(0)、@default(true)、@default([])、@default("{}") | 5 stars |
| 6 | @updatedAt | 自动维护更新时间 | updatedAt DateTime @updatedAt | 5 stars |
| 7 | @relation(...) | 关系字段(前面已详细讲) | 见前文 | 5 stars |
| 8 | @map("column_name") | 自定义数据库列名 | username String @map("user_name") | 4 stars |
| 9 | @@map("table_name") | 自定义表名 | @@map("users" | 4 stars |
| 10 | @db.Type | 强制指定 PostgreSQL 底层类型 | @db.Timestamptz、@db.VarChar(50)、@db.JsonB、@db.Date、@db.Uuid | 4 stars |
| 11 | @@index([field]) | 单字段索引 | @@index([email]) | 4 stars |
| 12 | @@index([field1, field2]) | 复合索引 | @@index([tenantId, createdAt]) | 4 stars |
| 13 | @@index([field], type: Gin) | GIN 索引(用于数组、jsonb、全文搜索) | @@index([tags], type: Gin)、@@index([metadata], type: Gin) | 4 stars |
| 14 | @@index([field], type: Hash) | Hash 索引(只适合 = 查询) | @@index([status], type: Hash) | 2 stars |
| 15 | @@index([field], type: Brin) | BRIN 索引(超大表时间序列) | @@index([createdAt], type: Brin) | 2 stars |
| 16 | @ignore | 完全忽略这个字段(不映射到数据库) | @ignore(极少用) | 1 star |
| 17 | @default(dbgenerated("sql")) | 使用数据库原生默认表达式 | @default(dbgenerated("gen_random_uuid()")) | 3 stars |
| 18 | @@fulltext([title, content]) | PostgreSQL 原生全文搜索索引 | @@fulltext([title, content]) | 3 stars |
疑问:我自己创建表的时候,也看到很多人创建表的时候,并没有同时创建索引。但是体感并没有太差,速度还是很快的。那么多大的数据量会开始有慢的感觉呢?
1,000 条以下:几乎无区别。10,000条左右开始有明显体感,如果你进行复杂的
JOIN(联表)或者ORDER BY(排序),速度会明显变慢。但是呢,所以也是有缺点的,在
INSERT、UPDATE或DELETE,数据库都要重新调整索引树。索引越多,写入越慢。索引有时会比数据本身还大。
所以要有选择的添加索引:
开发初期:先保证业务逻辑正确,除了
@id(主键会自动带索引)外,可以先不纠结索引。上线前夕:检查所有
findMany里的where条件涉及的字段,给它们补上索引。
如果使用的是postgresql,推荐使用postgresql原生enums,就是在postgresql数据库里面直接定义enums。然后在model定义的时候使用即可(可能需要先同步数据库)。
不推荐使用prisma的方式来定义enums,就是在schema.prisma里面千万不要定义enums。可以搜一下为什么,这里给一个理由:给已有枚举加一个新状态 → Prisma Migrate 直接 drop + recreate 整个 enum 类型,后果就是依赖这个 enum 的所有表都要重建 → 几千万行的大表锁表 40 分钟~3 小时,业务直接宕机。
而在postgresql原生里面进行修改,几乎没有什么影响,这是实际经验教训。
x
1model User {2 id Int @id @default(autoincrement())3 role Role @default(BASIC)4}56enum Role {7 BASIC8 ADMIN9}这里是一个schema文件的案例。
x
1// This is your Prisma schema file,2// learn more about it in the docs: https://pris.ly/d/prisma-schema34generator client {5 provider = "prisma-client-js"6}78datasource db {9 provider = "sqlite"10 url = env("DATABASE_URL")11}1213model User {14 id String @id @default(uuid())15 isAdmin Boolean @default(false)16 age Int17 name String18 email String @unique19 writtenPosts Post[] @relation("WrittenPosts")20 favoritePosts Post[] @relation("FavoritePosts")21 preferences UserPreference? @relation(fields: [userPreferenceId], references: [id])22 role Role @default(BASIC)23 userPreferenceId String? @unique2425 @@unique([name, age])26 @@index([email])27}2829model UserPreference {30 id String @id @default(uuid())31 emailUpdates Boolean32 user User?33}3435model Post {36 id String @id @default(uuid())37 title String38 averageRating Float // Also talk about BigInt, Decimal, Json, Unsupported, an39 createdAt DateTime @default(now())40 updatedAt DateTime @updatedAt41 author User @relation("WrittenPosts", fields: [authorId], references: [id])42 authorId String43 favoritedBy User? @relation("FavoritePosts", fields: [favoritedById], references: [id])44 favoritedById String?45 categories Category[]46}4748model Category {49 id String @id @default(uuid())50 name String @unique51 posts Post[]52}5354enum Role {55 BASIC56 ADMIN57}先记住下面最简单的操作,复杂操作查询文档即可,不要连最简单的操作都记不住。
create
xxxxxxxxxx71const user = await prisma.user.create({2 data: {3 email: 'kyle@test.com',4 name: 'Kyle',5 age: 276 }7})一对一、多对一新增使用connect来关联。
xxxxxxxxxx111// Create a new Post record and connect it to an existing User record.创建新post,同时将这个post关联到user.2// 注意,connect后面跟的只能是标了 @unique 或参与了 @@unique / @@id / @id 的字段,不一定只能是主键。34const user = await prisma.post.create({5 data: {6 title: 'Hello World',7 author: {8 connect: { email: 'alice@prisma.io' },9 },10 },11});createMany
xxxxxxxxxx151const user = await prisma.user.createMany({2 data: [3 {4 email: 'sally@test.com',5 name: 'Sally',6 age: 257 },8 {9 email: 'john@test.com',10 name: 'John',11 age: 1212 }13 ],14 skipDuplicates: true, // @unique 字段会校验是否唯一15})就记住这三个,不要嫌少,用的最多的就是这三种方法。
findUnique - Can also include/select
按 id、@unique 字段或 @@unique / @@id 组合查一条记录;
xxxxxxxxxx171// 1. 按主键查2prisma.user.findUnique({ where: { id: userId } })34// 2. 按任意 @unique 字段查(超好用!)5prisma.user.findUnique({ where: { email } }) // email @unique6prisma.user.findUnique({ where: { phone: "13800138000" } })7prisma.user.findUnique({ where: { slug: "alice" } })89// 3. 复合唯一索引(多租户必备),schema里面是这样定义的 @@unique([tenantId, email], name: "tenant_email")10prisma.user.findUnique({11 where: { tenantId_email: { tenantId: "t1", email: "test@163.com" } }12})1314// 4. 复合主键(如果你的表是 @@id([a, b]))15prisma.order.findUnique({16 where: { tenantId_orderNo: { tenantId: "t1", orderNo: "20250001" } }17})findFirst - Same as findMany but only gets first result
条件不是 schema 中的唯一键(例如多字段组合但没加 @@unique,或复杂过滤);
xxxxxxxxxx41prisma.post.findFirst({2 where: { published: true, author: { email: { contains: "gmail" } } },3 orderBy: { createdAt: 'desc' },4})findMany - Can also select/include
a. distinct - Can do distinct queries
b. take / skip - Can do pagination
c. orderBy - Can do sorting
xxxxxxxxxx61const results = await prisma.post.findMany({2 where: {3 title: { contains: 'Prisma' },4 },5 orderBy: { id: 'asc' },6})where条件:
xxxxxxxxxx761// 2.2 where 条件全家福(标量 + 数组 + JSONB + 关系)2prisma.post.findMany({3 where: {4 // 数值比较运算符(全部支持)5 viewCount: { 6 gt: 1000, // 大于 10007 gte: 1000, // 大于等于8 lt: 10000, // 小于9 lte: 10000, // 小于等于10 equals: 5000, // 等于(可以省略,直接写 viewCount: 5000 也行)11 not: 0, // 不等于12 },1314 // 字符串操作(底层走 PostgreSQL ILIKE / LIKE)15 title: { 16 contains: "Prisma", // 包含「Prisma」17 mode: "insensitive" // 不区分大小写(默认就是 insensitive,写出来更明确)18 },19 title: { startsWith: "2025" }, // 以「2025」开头20 title: { endsWith: ".md" }, // 以「.md」结尾21 title: { in: ["置顶", "精华"] }, // IN 数组22 title: { notIn: ["广告"] },2324 // 数组字段(String[] / Int[] 等)25 tags: { 26 has: "TypeScript", // 数组里包含某个值27 hasEvery: ["JS", "TS"], // 必须同时包含这几个28 hasSome: ["Vue", "React"], // 包含其中任意一个即可29 isEmpty: true, // 空数组30 },3132 // JSONB 字段操作(超级实用)33 metadata: { 34 equals: { plan: "pro" }, // 完全相等35 path: ["plan"], equals: "pro" // metadata->>'plan' = 'pro'36 path: ["settings", "theme"], equals: "dark", // 嵌套路径37 string_contains: "vip", // JSON 字符串里包含 vip38 string_starts_with: "gold", // 以 gold 开头39 string_ends_with: "member",40 },4142 // 关系存在性查询(神技!不需要 join)43 author: { 44 is: { isVip: true } , // 作者是 VIP45 isNot: { status: "banned" }, // 作者不是被封禁的46 },47 comments: { 48 some: { content: { contains: "赞" } }, // 至少有一条评论含「赞」49 none: { authorId: userId }, // 这篇文章没人评论过(我没评论)50 every: { content: { not: { contains: "差评" } } }, // 所有评论都不含差评51 },52 likedBy: { some: { id: userId } }, // 我点过赞的文章5354 // 逻辑组合55 AND: [56 { published: true },57 { createdAt: { gte: new Date("2025-01-01") } },58 ],59 OR: [60 { title: { contains: "Prisma" } },61 { content: { contains: "ORM" } },62 ],63 NOT: { status: "deleted" },64 },6566 // 排序(支持多字段)67 orderBy: [68 { viewCount: 'desc' }, // 阅读量倒序69 { createdAt: 'asc' }, // 时间正序(同分再按时间)70 ],7172 // 分页方式推荐(游标分页 > offset)73 take: 20,74 skip: 0,75 // cursor: { id: "last_seen_id" }, // 游标分页示例76});include/select方式:
xxxxxxxxxx521// 2.3 include vs select 详细注释版2prisma.user.findUnique({3 where: { id: userId },45 // include 方式:全部字段 + 关联(简单粗暴,适合快速开发)6 include: {7 posts: {8 where: { published: true },9 orderBy: { createdAt: 'desc' },10 take: 10,11 include: {12 tags: true, // 标签全量13 _count: { select: { comments: true, likedBy: true } }, // 评论数、点赞数14 },15 },16 profile: true, // 一对一 profile 全量17 _count: { // 统计关联数量18 select: {19 posts: true, // 文章总数20 likedPosts: true, // 点过赞的文章数21 },22 },23 },2425 // select 方式:精准控制字段(生产环境强烈推荐!性能更好)26 select: {27 id: true,28 email: true,29 name: true,30 createdAt: true,3132 posts: {33 select: {34 id: true,35 title: true,36 createdAt: true,37 _count: { select: { comments: true } },38 },39 where: { published: true },40 orderBy: { createdAt: 'desc' },41 take: 5,42 },4344 profile: {45 select: { bio: true },46 },4748 _count: {49 select: { posts: true, likedPosts: true },50 },51 },52});聚合、统计、分组:
xxxxxxxxxx291// 2.4 聚合、统计、分组(2025 年最常用写法)2const result = await prisma.post.aggregate({3 where: { authorId: userId },4 _count: { id: true }, // 文章总数5 _avg: { viewCount: true }, // 平均阅读量6 _sum: { viewCount: true }, // 总阅读总量7 _min: { viewCount: true },8 _max: { viewCount: true },9});1011const group = await prisma.post.groupBy({12 by: ['authorId'], // 按作者分组13 _count: { id: true }, // 每人文章数14 _avg: { viewCount: true },15 having: {16 id: { _count: { gt: 5 } }, // 只返回文章数 >5 的作者17 },18 orderBy: { _count: { id: 'desc' } },19});2021const count = await prisma.post.count({22 where: {23 published: true,24 AND: [25 { createdAt: { gte: new Date("2025-01-01") } },26 { createdAt: { lte: new Date("2025-12-31") } },27 ],28 },29});这部分是关联过滤,还是蛮复杂的,看不懂也很正常。先了解。
xxxxxxxxxx141const result = await prisma.user.findMany({2 where: {3 post: {4 every: {5 published: true6 },7 some: {8 content: {9 contains: "Prisma"10 }11 }12 }13 }14})xxxxxxxxxx631prisma.post.findMany({2 where: {3 // 1. some:关联记录中「至少有一条」满足条件(最常用!)4 comments: {5 some: {6 content: { contains: "厉害" }, // 有评论说“厉害”7 // OR: [{ author: { isVip: true } }, { content: { contains: "1000+" } }]8 },9 },1011 likedBy: {12 some: { id: currentUserId }, // 我点过赞的文章13 },1415 tags: {16 some: { name: { in: ["TypeScript", "Prisma"] } }, // 至少有一个标签是 TS 或 Prisma17 },1819 // 2. every:关联记录「全部」都要满足条件(常用于审核/权限)20 comments: {21 every: {22 status: "approved", // 所有评论都已通过审核(没有一条待审或拒绝)23 },24 },2526 // 3. none:关联记录「一条都不」满足条件 = 完全没有(神技!)27 comments: {28 none: { authorId: currentUserId }, // 我一条评论都没发过29 },3031 likedBy: {32 none: { id: currentUserId }, // 我没点过赞(可用于“显示点赞按钮”)33 },3435 reports: {36 none: { status: "pending" }, // 这篇文章没有任何待处理的举报 → 可以正常展示37 },3839 // 4. is:对端记录「本身」满足条件(一对多/一对一常用)40 author: {41 is: {42 isVip: true, // 作者是 VIP43 status: "active", // 作者状态正常44 // AND: [{ isVip: true }, { createdAt: { gte: new Date("2025-01-01") } }],45 },46 },4748 profile: {49 is: { bio: { not: null } }, // 一对一:用户填了 bio50 },5152 // 5. isNot:对端记录「本身」不满足条件53 author: {54 isNot: {55 status: "banned", // 作者没被封号56 // OR: [{ status: "deleted" }, { isVip: false }]57 },58 },59 },60 // 下面这些可以随便混着用61 orderBy: { createdAt: 'desc' },62 take: 20,63});| 关键词 | 含义 | 对应 SQL 感觉 |
|---|---|---|
| some | 至少一条 | EXISTS / IN |
| every | 全部满足 | NOT EXISTS(不满足的) |
| none | 一条都不 | NOT EXISTS |
| is | 对端本身满足 | 内层 WHERE |
| isNot | 对端本身不满足 | 内层 WHERE + NOT |
总体原则是先使用where条件,然后使用data更新值。
update - Can also include/select
a. Only updates 1 record
b. Must include data and where
①基础更新(单表)
xxxxxxxxxx141// 普通字段更新2await prisma.user.update({3 where: { id: userId }, // 任何唯一约束都行4 data: {5 name: "新名字",6 avatar: "https://xxx.jpg",7 status: "active",8 score: { increment: 10 }, // +109 viewCount: { decrement: 1 }, // -110 roles: { set: ["user", "admin"] }, // 完全替换数组11 roles: { push: "moderator" }, // 追加一个12 metadata: { set: { plan: "pro" } }, // JSON 整体替换13 },14});②一对多:修改文章标题 + 同时关联/取消关联作者
xxxxxxxxxx171await prisma.post.update({2 where: { id: postId },3 data: {4 title: "2025 年 Prisma 还是香",5 content: "更新了内容",67 // 更换作者(connect)8 author: { connect: { email: "new@author.com" } },910 // 解除作者关系(disconnect)11 // author: { disconnect: true },1213 // 置为 null(如果外键可空)14 // author: { disconnect: true },15 // authorId: null,16 },17});③更复杂的操作:多对多、upsert方法,用的时候查询即可。
数字操作,感觉不实用。
increment
decrement
multiply
divide
xxxxxxxxxx81const updatedUser = await prisma.post.update({2 where: { "name": "Kyle" },3 data: {4 age: {5 increment: 16 }7 }8})delete - Can also select/include
Only deletes 1 record
xxxxxxxxxx31await prisma.post.delete({2 where: { id: postId }, // 任何唯一约束都行3});deleteMany - Can also select/include
x
1// 删除 userId 用户在 2023 年前的所有文章2await prisma.post.deleteMany({3 where: {4 authorId: userId,5 createdAt: { lt: new Date("2023-01-01") }, 6 },7});@ 或 @@ 开头属性Prisma 中所有以 @ 或 @@ 开头的东西都叫 Attributes(属性),可以作用于:
@@@下面给你一个 最完整、最清晰、业务开发最常用的列表。
@ 开头用于 字段(column)级别 的功能,比如主键、默认值、映射等。
| 属性 | 作用 |
|---|---|
@id | 将字段设置为主键 |
@unique | 设置字段为唯一索引 |
@@id([]) | 复合主键(表级)→ 用 @@,但属于 ID 类 |
| 属性 | 作用 |
|---|---|
@default(value) | 字段默认值,如字符串、数字、布尔、函数 |
@default(now()) | 时间戳默认值 |
@default(uuid()) | 生成 UUID |
@default(cuid()) | 生成 cuid(prisma规范,强烈推荐使用) |
@updatedAt | 每次更新记录时自动更新该字段 |
| 属性 | 作用 |
|---|---|
@relation() | 定义关系,指定 foreign key 字段和引用字段 |
例:
xxxxxxxxxx11author User @relation(2 fields: [authorId], 3 references: [id]4)| 属性 | 作用 |
|---|---|
@map("column_name") | 把 Prisma 字段映射到数据库列名 |
@@map("table_name") | 映射模型名到数据库表(表级) |
| 属性 | 作用 |
|---|---|
@db.VarChar(255) | 指定数据库实际类型(适配 PostgreSQL, SQLite, MySQL 等) |
@@ 开头用于 模型(表)级别 的功能,比如复合索引、表名映射等。
| 属性 | 作用 |
|---|---|
@@map("table_name") | 映射 Prisma 模型到数据库表 |
| 属性 | 作用 |
|---|---|
@@index([field1, field2]) | 普通索引 |
@@unique([field1, field2]) | 复合唯一索引 |
| 属性 | 作用 |
|---|---|
@@id([field1, field2]) | 设置复合主键 |
注意:复合主键不能和
@id共存。
一般用于双向关系中的自引用模型:
xxxxxxxxxx21parent Comment? @relation("CommentReplies")2replies Comment[] @relation("CommentReplies")虽然写在 @relation 里,但属于模型关系配置范畴。
字段级(@)
| 属性 | 用途 |
|---|---|
@id | 主键 |
@unique | 唯一约束 |
@default() | 默认值 |
@updatedAt | 自动更新时间 |
@relation() | 外键、关系 |
@map() | 字段映射到数据库列 |
@db.* | 数据库原生类型 |
模型级(@@)
| 属性 | 用途 |
|---|---|
@@map() | 模型映射到数据库表 |
@@index() | 索引 |
@@unique() | 复合唯一 |
@@id() | 复合主键 |